#pragma once

/// @file userver/proto-structs/oneof.hpp
/// @brief @copybrief proto_structs::Oneof

#include <optional>
#include <utility>
#include <variant>

#include <userver/proto-structs/exceptions.hpp>
#include <userver/proto-structs/type_mapping.hpp>

USERVER_NAMESPACE_BEGIN

namespace proto_structs {

/// @brief Wrapper for `oneof` protobuf message fields.
/// @tparam TFields `oneof` field types
template <typename... TFields>
class Oneof {
public:
    /// @brief Type of this `Oneof`.
    using Base = Oneof;

    /// @brief Number of fields in the oneof.
    static constexpr std::size_t kSize = sizeof...(TFields);

    static_assert(kSize > 0, "Oneof should contain at least one field");

    /// @brief Creates oneof without any field set.
    constexpr Oneof() = default;

    /// @brief Creates oneof wrapper initializing @a index field.
    template <std::size_t Index, typename... TArgs>
    constexpr Oneof(std::in_place_index_t<Index> index, TArgs&&... args)
        : storage_(std::variant<TFields...>(index, std::forward<TArgs>(args)...)) {}

    /// @brief Returns zero-based index of the alternative held by the oneof.
    ///
    /// If oneof does not contain any field, returns @ref proto_structs::kOneofNpos .
    [[nodiscard]] constexpr std::size_t GetIndex() const noexcept { return storage_ ? storage_->index() : kOneofNpos; }

    /// @brief Returns `true` of oneof contains @a index field.
    [[nodiscard]] constexpr bool Contains(std::size_t index) const noexcept {
        return storage_ && storage_->index() == index;
    }

    /// @brief Returns `true` of oneof contains some field.
    [[nodiscard]] constexpr bool ContainsAny() const noexcept { return storage_.has_value(); }

    /// @brief Returns `Index` field.
    /// @throws OneofAccessError if `Index` field is not set.
    template <std::size_t Index>
    [[nodiscard]] constexpr const OneofAlternativeType<Index, Oneof>& Get() const& {
        if (!Contains(Index)) {
            throw OneofAccessError(Index);
        }

        return std::get<Index>(*storage_);
    }

    /// @brief Returns `Index` field.
    /// @throws OneofAccessError if `Index` field is not set.
    template <std::size_t Index>
    [[nodiscard]] constexpr OneofAlternativeType<Index, Oneof>& Get() & {
        if (!Contains(Index)) {
            throw OneofAccessError(Index);
        }

        return std::get<Index>(*storage_);
    }

    /// @brief Returns `Index` field.
    /// @throws OneofAccessError if `Index` field is not set.
    template <std::size_t Index>
    [[nodiscard]] constexpr OneofAlternativeType<Index, Oneof>&& Get() && {
        if (!Contains(Index)) {
            throw OneofAccessError(Index);
        }

        return std::get<Index>(*std::move(storage_));
    }

    /// @brief Initializes `Index` field in-place and returns it.
    /// @tparam TArgs arguments to pass to `Index` field constructor
    template <std::size_t Index, typename... TArgs>
    constexpr OneofAlternativeType<Index, Oneof>& Emplace(TArgs&&... args) {
        storage_.emplace(std::in_place_index<Index>, std::forward<TArgs>(args)...);
        return std::get<Index>(*storage_);
    }

    template <std::size_t Index>
    constexpr void Set(const OneofAlternativeType<Index, Oneof>& value) {
        Emplace<Index>(value);
    }

    template <std::size_t Index>
    constexpr void Set(OneofAlternativeType<Index, Oneof>&& value) {
        Emplace<Index>(std::move(value));
    }

    /// @brief If field at `Index` is not set, sets it. Returns a mutable reference to the field.
    template <std::size_t Index>
    [[nodiscard]] constexpr OneofAlternativeType<Index, Oneof>& GetMutable() {
        if (!Contains(Index)) {
            Emplace<Index>();
        }

        return std::get<Index>(*storage_);
    }

    /// @brief Clear field `Index` if it is set.
    void Clear(std::size_t index) noexcept {
        if (Contains(index)) {
            ClearOneof();
        }
    }

    /// @brief Clears oneof.
    void ClearOneof() noexcept { storage_.reset(); }

    /// @brief Returns `true` if oneof contains some field.
    constexpr explicit operator bool() const noexcept { return ContainsAny(); }

    /// @brief Comparison to another oneof of the same type.
    bool operator==(const Oneof& other) const = default;

private:
    std::optional<std::variant<TFields...>> storage_;
};

}  // namespace proto_structs

USERVER_NAMESPACE_END
